package net.qiqbframework.messaging;

import net.qiqbframework.common.IdentifierFactory;
import net.qiqbframework.messaging.task.CurrentMessageTask;
import net.qiqbframework.serialization.SerializedObject;
import net.qiqbframework.serialization.SerializedObjectHolder;
import net.qiqbframework.serialization.Serializer;

import java.util.Map;

public class GenericMessage<T> extends AbstractMessage<T> {

    private MetaData metaData;
    private final Class<T> payloadType;
    private final T payload;
    private transient volatile SerializedObjectHolder serializedObjectHolder;


    public static Message<?> asMessage(Object payloadOrMessage) {
        if (payloadOrMessage instanceof Message) {
            return (Message<?>) payloadOrMessage;
        } else {
            return new GenericMessage<>(payloadOrMessage);
        }
    }

    public GenericMessage(T payload) {
        this(payload, MetaData.emptyInstance());
    }


    public GenericMessage(T payload, Map<String, ?> metaData) {
        this(getDeclaredPayloadType(payload), payload, metaData);
    }


    public GenericMessage(Class<T> declaredPayloadType, T payload, Map<String, ?> metaData) {
        this(IdentifierFactory.getInstance().generate(), declaredPayloadType, payload,
                CurrentMessageTask.correlationData().mergedWith(MetaData.of(metaData)));
    }


    public GenericMessage(String identifier, T payload, Map<String, ?> metaData) {
        this(identifier, getDeclaredPayloadType(payload), payload, metaData);
    }

    public GenericMessage(String identifier, Class<T> declaredPayloadType, T payload, Map<String, ?> metaData) {
        super(identifier);
        this.metaData = MetaData.of(metaData);
        this.payload = payload;
        this.payloadType = declaredPayloadType;
    }

    private GenericMessage(GenericMessage<T> original, MetaData metaData) {
        super(original.getIdentifier());
        this.payload = original.getPayload();
        this.payloadType = original.getPayloadType();
        this.metaData = metaData;
    }

    /**
     * Extract the {@link Class} of the provided {@code payload}. If {@code payload == null} this function returns
     * {@link Void} as the payload type.
     *
     * @param payload the payload of this {@link Message}
     * @return the declared type of the given {@code payload} or {@link Void} if {@code payload == null}
     */
    @SuppressWarnings("unchecked")
    private static <T> Class<T> getDeclaredPayloadType(T payload) {
        return payload != null ? (Class<T>) payload.getClass() : (Class<T>) Void.class;
    }

    @Override
    public MetaData getMetaData() {
        return metaData;
    }

    @Override
    public T getPayload() {
        return payload;
    }

    @Override
    public Class<T> getPayloadType() {
        return payloadType;
    }

    @Override
    protected void withMetaData(MetaData metaData) {
        this.metaData = this.metaData.mergedWith(metaData);
    }

    @Override
    public <R> SerializedObject<R> serializePayload(Serializer serializer, Class<R> expectedRepresentation) {
        return serializedObjectHolder().serializePayload(serializer, expectedRepresentation);
    }

    @Override
    public <R> SerializedObject<R> serializeMetaData(Serializer serializer, Class<R> expectedRepresentation) {
        return serializedObjectHolder().serializeMetaData(serializer, expectedRepresentation);
    }

    private SerializedObjectHolder serializedObjectHolder() {
        if (serializedObjectHolder == null) {
            serializedObjectHolder = new SerializedObjectHolder(this);
        }
        return serializedObjectHolder;
    }
}
